package com.ho1ho.audioexample.utils;

import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.util.Log;

import com.ho1ho.audioexample.CustomApplication;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;

/**
 * Created by Michael Leo <y@ho1ho.com><br>
 * Date: 2020/02/10 11:15
 * <p>
 * Code is far away from bug with the animal protecting
 * </p>
 * <pre>
 * ----------Dragon be here!----------/
 *    ┏┓   ┏┓
 *   ┏┛┻━━━┛┻┓
 *   ┃       ┃
 *   ┃   ━   ┃
 *   ┃ ┳┛ ┗┳ ┃
 *   ┃       ┃
 *   ┃   ┻   ┃
 *   ┃       ┃
 *   ┗━┓   ┏━┛
 *     ┃   ┃神兽保佑
 *     ┃   ┃代码无BUG！
 *     ┃   ┗━━━┓
 *     ┃       ┣┓
 *     ┃       ┏┛
 *     ┗┓┓┏━┳┓┏┛
 *      ┃┫┫ ┃┫┫
 *      ┗┻┛ ┗┻┛
 * ━━━━━━神兽出没━━━━━━
 * </pre>
 */
public class AacDecoder {
    private static final String TAG = AacDecoder.class.getSimpleName();

    private BufferedOutputStream mAacOs;
    private long presentationTimeUs = 0L;

    private AudioTrack mAudioTrack;
    private MediaCodec mAudioDecoder;

    public AacDecoder(int sampleRate, int channelConfig, int audioFormat, byte[] csd0, int trackBufferSize) {
        Log.e(TAG, "trackBufferSize=" + trackBufferSize);

        initAudioDecoder(sampleRate, csd0);
        initAudioTrack(sampleRate, channelConfig, audioFormat, trackBufferSize);
    }

    public AacDecoder(int sampleRate, int channelConfig, int audioFormat, byte[] csd0) {
        this(sampleRate, channelConfig, audioFormat, csd0,
                AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat) * 2);
    }

    /**
     * Write audio data to disk<br>
     * <br>
     * DEBUG ONLY
     *
     * @param audioData The audio data to be written.
     */
    public void writeDataToDisk(byte[] audioData) {
        try {
            if (mAacOs != null) {
                if ("END-OF-AUDIO".equals(new String(audioData, StandardCharsets.UTF_8))) {
                    Log.e(TAG, "END-OF-AUDIO close stream.");
                    mAacOs.flush();
                    mAacOs.close();
                    return;
                }
                mAacOs.write(audioData);
            } else {
                Log.e(TAG, "mAacOs is null");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * DEBUG ONLY
     */
    public void initOutputStream() {
        String outputFolder = Objects.requireNonNull(CustomApplication.instance.getExternalFilesDir(null)).getAbsolutePath() + File.separator + "leo-audio";
        File folder = new File(outputFolder);
        if (!folder.exists()) {
            boolean succ = folder.mkdirs();
            if (!succ) {
                Log.e(TAG, "Can not create output file: " + outputFolder);
            }
        }
        File aacFile = new File(outputFolder, "received-original.aac");
        final String aacFilename = aacFile.getAbsolutePath();

        try {
            mAacOs = new BufferedOutputStream(new FileOutputStream(aacFilename));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void initAudioTrack(int sampleRate, int channelConfig, int audioFormat, int bufferSize) {
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                sampleRate,
                channelConfig,
                audioFormat,
                bufferSize,
                AudioTrack.MODE_STREAM);

        mAudioTrack.play();
    }

    // https://juejin.im/post/5c36bbad6fb9a049d975676b
    // https://stackoverflow.com/questions/56106877/how-to-decode-aac-formatmp4-audio-file-to-pcm-format-in-android
    // https://www.jianshu.com/p/b30d6a4f745b
    // https://blog.csdn.net/lavender1626/article/details/80431902
    // http://sohailaziz05.blogspot.com/2014/06/mediacodec-decoding-aac-android.html
    private void initAudioDecoder(int sampleRate, byte[] csd0) {
        try {
//            String folder = Objects.requireNonNull(CustomApplication.instance.getExternalFilesDir(null)).getAbsolutePath() + File.separator + "leo-audio";
//            File mFilePath = new File(folder, "original.aac");

//            mMediaExtractor = new MediaExtractor();
//            mMediaExtractor.setDataSource(mFilePath.getAbsolutePath());

//            MediaFormat mediaFormat = mMediaExtractor.getTrackFormat(0);
//            String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
//            if (mime.startsWith("audio")) {
//                mMediaExtractor.selectTrack(0);

//                mediaFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_AAC);
//                mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
//                mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, AacEncoder.DEFAULT_SAMPLE_RATES);
//                mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, AacEncoder.DEFAULT_BIT_RATES);
//                mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, CHANNEL_IN);
//                mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 1);
//                mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);

            mAudioDecoder = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
            MediaFormat mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC,
                    sampleRate,
                    1);
            mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
            mediaFormat.setInteger(MediaFormat.KEY_IS_ADTS, 1);

            // ByteBuffer key
            // AAC Profile 5bits | SampleRate 4bits | Channel Count 4bits | Others 3bits（Normally 0)
            // Example: AAC LC，44.1Khz，Mono. Separately values: 2，4，1.
            // Convert them to binary value: 0b10, 0b100, 0b1
            // According to AAC required, convert theirs values to binary bits:
            // 00010 0100 0001 000
            // The corresponding hex value：
            // 0001 0010 0000 1000
            // So the csd_0 value is 0x12，0x08
            ByteBuffer csd_0 = ByteBuffer.wrap(csd0);
            // Set ADTS decoder information.
            mediaFormat.setByteBuffer("csd-0", csd_0);

            mAudioDecoder.configure(mediaFormat, null, null, 0);
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (mAudioDecoder == null) {
            Log.e(TAG, "mAudioDecoder is null");
            return;
        }
        // Start MediaCodec. Waiting to receive data.
        mAudioDecoder.start();
    }

    public void decodeAndPlay(byte[] audioData) {
        try {
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

            ByteBuffer inputBuffer;
            ByteBuffer outputBuffer;

            // See the dequeueInputBuffer method in document to confirm the timeoutUs parameter.
            int inputIndex = mAudioDecoder.dequeueInputBuffer(-1);
            Log.w(TAG, "inputIndex=" + inputIndex);
            if (inputIndex < 0) {
                return;
            }
            inputBuffer = mAudioDecoder.getInputBuffer(inputIndex);
            if (inputBuffer != null) {
                // Clear exist data.
                inputBuffer.clear();
                // Put pcm audio data to encoder.
                inputBuffer.put(audioData);
//        inputBuffer.limit(audioData.length);
                Log.i(TAG, "Received aac data[" + audioData.length + "]=" + Arrays.toString(audioData));
            }
//            int sampleSize = mMediaExtractor.readSampleData(inputBuffer, 0);
//            if (sampleSize > 0) {
            mAudioDecoder.queueInputBuffer(inputIndex, 0, audioData.length, AacEncoder.computePresentationTimeUs(presentationTimeUs)  /*mMediaExtractor.getSampleTime()*/, 0); //通知解码器 解码
            presentationTimeUs += 1;
//                mMediaExtractor.advance(); // MediaExtractor. Move to next sample.
//            } else {
//                mAudioDecoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
//                isFinish = true;
//            }

            // Start decoding and get output index
            int outputIndex = mAudioDecoder.dequeueOutputBuffer(bufferInfo, 0);
            Log.e(TAG, "outputIndex=" + outputIndex);

            byte[] chunkPCM;
            // Get decoded data in bytes
            while (outputIndex >= 0) {
                outputBuffer = mAudioDecoder.getOutputBuffer(outputIndex);
                chunkPCM = new byte[bufferInfo.size];
                if (outputBuffer != null) {
                    outputBuffer.get(chunkPCM);
                } else {
                    Log.w(TAG, "outputBuffer is null");
                }
                // Must clear decoded data before next loop. Otherwise, you will get the same data while looping.
                if (chunkPCM.length > 0) {
                    Log.i(TAG, "PCM data[" + chunkPCM.length + "]=" + Arrays.toString(chunkPCM));
                    // Play decoded audio data in PCM
                    mAudioTrack.write(chunkPCM, 0, chunkPCM.length);
                }
                mAudioDecoder.releaseOutputBuffer(outputIndex, false);
                outputIndex = mAudioDecoder.dequeueOutputBuffer(bufferInfo, 0);//再次获取数据
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void release() {
        if (mAudioTrack != null) {
            mAudioTrack.stop();
            mAudioTrack.release();
            mAudioTrack = null;
        }

        if (mAudioDecoder != null) {
            mAudioDecoder.stop();
            mAudioDecoder.release();
            mAudioDecoder = null;
        }
    }

    public AudioTrack getAudioTrack() {
        return mAudioTrack;
    }

    public void setAudioTrack(AudioTrack mAudioTrack) {
        this.mAudioTrack = mAudioTrack;
    }

    public MediaCodec getAudioDecoder() {
        return mAudioDecoder;
    }

    public void setAudioDecoder(MediaCodec mAudioDecoder) {
        this.mAudioDecoder = mAudioDecoder;
    }
}
